Vamos falar nesta aula sobre herança, que é um dos conceitos mais importantes e utilizados dentro da orientação a objetos. A herança traz várias vantagens como, por exemplo, a reutilização de código que reduz a manutenção de software e permite criar aplicações mais complexas.
"Começamos a insistir na ideia de herança como uma maneira de permitir que novatos desenvolvessem com base em frameworks que poderiam ser projetados somente por experts" (Alan Kay, The Early History of Smalltalk)
Também vamos falar sobre herança múltipla. Muitos programadores chegam ao Python do Java sem ter visto herança múltipla na prática, por esse motivo serão usados exemplos didáticos sobre esse tema usando um projeto Python importante: o framework web Django.
Na aula passada vimos o seguinte exemplo da classe Cão
. Hoje vamos usar herança para especificar raças diferentes de cachorros:
In [1]:
class Cão:
qtd_patas = 4
carnívoro = True
nervoso = False
def __init__(self, nome):
self.nome = nome
def latir(self, vezes=1):
""" Latir do cão. Quanto mais nervoso mais late. """
vezes += self.nervoso * vezes
latido = 'Au! ' * vezes
print('{}: {}'.format(self.nome, latido))
Vamos brincar um pouco com o cão:
In [2]:
rex = Cão('Rex')
rex.qtd_patas
Out[2]:
In [3]:
rex.nome
Out[3]:
In [4]:
rex.latir()
In [5]:
rex.latir(5)
In [6]:
rex.nervoso
Out[6]:
In [7]:
rex.nervoso = True
rex.latir()
In [8]:
rex.latir(10)
Agora vamos criar a classe GoldenRetriever
como subclasse de cão. Os cachorros da raça golden retriever são conhecidos por serem muito inteligente e amigáveis. Seu nome retriever (pegador) por sua habilidade de pegar os alvos em jogos de caça sem danificá-los. Por esse motivo vamos implementar um método que permita os cães dessa raça pegar e devolver itens.
In [9]:
class GoldenRetriever(Cão):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.itens = []
def pega(self, item):
""" busca/pega um item quando ordenado """
self.itens.append(item)
print('{} pegou {}'.format(self.nome, item))
def devolve(self, item=None):
""" devolve um item, caso o item não seja especificado retorna o último que pegou """
if not self.itens:
print('{} não está segurando item algum!'.format(self.nome))
return
if not item:
item = self.itens.pop()
elif item not in self.itens:
print('{} não está segurando {}!'.format(self.nome, item))
return
else:
self.itens.remove(item)
print('{} devolve {}'.format(self.nome, item))
return item
Linha 1: especificamos que a classe GoldenRetriever
herda de Cão
.
Linha 2: sobrescrevemos o construtor da superclasse.
Linha 3: repassamos os argumentos para o inicializador de Cão
.
Linha 4: criamos um novo atributo itens
às instâncias de GoldenRetriever
Por herdar da classe Cão
a classe GoldenRetriever
recebe todos os métodos e atributos do primeiro:
In [10]:
nana = GoldenRetriever('Nana')
nana.nome
Out[10]:
In [11]:
nana.nervoso
Out[11]:
In [12]:
nana.carnívoro
Out[12]:
In [13]:
nana.latir()
In [14]:
nana.latir(5)
E temos acesso aos métodos e atributos de GoldenRetriever
:
In [15]:
nana.itens
Out[15]:
In [16]:
nana.pega('bola')
In [17]:
nana.itens
Out[17]:
In [18]:
nana.devolve()
Out[18]:
In [19]:
nana.devolve()
Python não tem suporte "nativo" a sobrecarga de métodos de mesmo nome. Isso se dá pois a linguagem possui outros métodos de emular essa funcionalidade, como: argumentos padrão e empacotamento de argumentos posicionais e nomeados.
No exemplo anterior, o método GoldenRetriever.devolve()
poderia ser escrito com dois metódos: um que não recebe item GoldenRetriever.devolve(self)
e outro que recebe um item GoldenRetriever.devolve(self, item)
. Isso não foi necessário, pois usamos argumento padrão.
Mas, se analisarmos a função devolve com cuidado logo percebemos que ela faz duas coisas diferentes - assim como em uma lista temos os métodos list.pop()
e list.remove()
e não somente um list.remove()
que funciona para todos os casos. Seguindo as melhores práticas de programação seria melhor escrever duas funções diferentes para isso:
In [43]:
class GoldenRetriever(Cão):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.itens = []
def pega(self, item):
""" busca/pega um item quando ordenado """
self.itens.append(item)
print('{} pegou {}'.format(self.nome, item))
def devolve(self, item):
""" devolve um item, caso o item não seja especificado retorna o último que pegou """
if not self.itens:
raise ValueError('{} não está segurando item algum!'.format(self.nome))
if item not in self.itens:
raise ValueError('{} não está segurando {}!'.format(self.nome, item))
self.itens.remove(item)
print('{} devolve {}'.format(self.nome, item))
return item
def devolve_ultimo(self):
if not self.itens:
raise ValueError('{} não está segurando item algum!'.format(self.nome))
return self.itens.pop()
Vamos testar a nova funcionalidade:
In [90]:
toto = GoldenRetriever('Totó')
toto.nome
Out[90]:
In [91]:
toto.itens
Out[91]:
In [103]:
toto.pega('chinelo')
In [104]:
toto.pega('bola')
In [105]:
toto.itens
Out[105]:
In [106]:
toto.devolve_ultimo()
Out[106]:
In [107]:
toto.devolve('meia')
In [108]:
toto.devolve('chinelo')
Out[108]:
In [109]:
toto.devolve_ultimo()
Devemos notar que usamos *args
e **kwargs
na definição da função (linha 2) para repassar a instanciação dos argumentos à classe Cão
, dessa maneira se modificarmos esta classe e adicionarmos outros parâmetros, então GoldenRetriever
também aceitará esses parâmetros, como podemos ver no seguinte exemplo que redefinimos a classe Cão
:
In [110]:
class Cão:
qtd_patas = 4
carnívoro = True
nervoso = False
def __init__(self, nome, data_nascimento=None):
self.nome = nome
self.data_nascimento = data_nascimento
def latir(self, vezes=1):
""" Latir do cão. Quanto mais nervoso mais late. """
vezes += self.nervoso * vezes
latido = 'Au! ' * vezes
print('{}: {}'.format(self.nome, latido))
Precisamos recarregar a classe GoldenRetriever
para ela herdar da nova superclasse:
In [111]:
class GoldenRetriever(Cão):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.itens = []
def pega(self, item):
""" busca/pega um item quando ordenado """
self.itens.append(item)
print('{} pegou {}'.format(self.nome, item))
def devolve(self, item):
""" devolve um item, caso o item não seja especificado retorna o último que pegou """
if not self.itens:
raise ValueError('{} não está segurando item algum!'.format(self.nome))
if item not in self.itens:
raise ValueError('{} não está segurando {}!'.format(self.nome, item))
self.itens.remove(item)
print('{} devolve {}'.format(self.nome, item))
return item
def devolve_ultimo(self):
if not self.itens:
raise ValueError('{} não está segurando item algum!'.format(self.nome))
return self.itens.pop()
In [112]:
from datetime import date
totó = GoldenRetriever('Totó', date(2016, 4, 4))
totó.data_nascimento
Out[112]:
In [113]:
print(totó.data_nascimento)
In [115]:
fido = GoldenRetriever('Fido')
print(fido.data_nascimento)
Vamos criar mais uma subclasse de Cão
. Dessa vez iremos criar a classe Pinscher
, essa raça de cachorro tem a quantidade de raiva inversamente proporcional ao seu tamanho, ou seja, são muito nervosos! E também latem mais cães de outras raças:
In [116]:
class Pinscher(Cão):
nervoso = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def latir(self, vezes=1):
vezes *= 2
super().latir(vezes)
In [117]:
mimi = Pinscher('Mimi')
mimi.nervoso
Out[117]:
In [118]:
mimi.nome
Out[118]:
In [119]:
mimi.latir()
In [120]:
mimi.latir(5)
Para finalizar vamos criar a classe SãoBernardo
que representa cães dessa mesma raça. Esta ficou famosa por diversos filmes de humor que passou dezenas de vezes em certo canal da TV aberta brasileira. Há lendas que dizem que, em países que nevam, o cão São Bernardo leva um pequeno barril de conhaque para resgatar viajantes perdidos na neve:
In [61]:
class SãoBernardo(Cão):
def __init__(self, *args):
super().__init__(*args)
self.doses = 10
def servir(self):
if self.doses == 0:
raise ValueError("'Cabou a birita!")
self.doses -= 1
print('{} serve a birita (restam {} doses)'.format(self.nome, self.doses))
In [62]:
sansao = SãoBernardo('Sansão')
sansao.servir()
In [63]:
sansao.doses = 1
sansao.servir()
In [64]:
sansao.servir()
O Python possui duas funções para verificar instâncias: isinstance(obj, cls)
que checa se uma classe é uma instância da classe ou de suas superclasses:
In [65]:
isinstance(sansao, SãoBernardo)
Out[65]:
In [72]:
isinstance(sansao, Cão)
Out[72]:
In [75]:
isinstance(totó, SãoBernardo)
Out[75]:
In [77]:
isinstance(totó, GoldenRetriever)
Out[77]:
In [76]:
isinstance(totó, Cão)
Out[76]:
Para verificar se o objeto é exatamente o tipo desejado é necessário usar a função embutida type()
:
In [78]:
type(sansao) is SãoBernardo
Out[78]:
In [79]:
type(sansao) is Cão
Out[79]:
In [81]:
type(totó) is Cão
Out[81]:
Por fim também existe a função issubclass(class, classinfo)
que verifica se uma classe é derivada de outra:
In [87]:
issubclass(type(sansao), Cão)
Out[87]:
In [88]:
issubclass(bool, int)
Out[88]:
Spoiler: o tipo bool
é derivado do tipo int
. (veremos mais sobre isso na aula sobre python data model)
In [89]:
issubclass(float, int)
Out[89]:
Porém o tipo float
não.
O UML é um padrão de modelagem de software para diversos campos do desenvolvimento. O padrão possui diversos diagramas para expressar lógica de negócio e estrutura de programas. Para este curso veremos apenas Diagramas de Classe.
Vamos ver como fica o nosso código de cães em diagramas de classe:
A classe é representada por retângulo e é dividida por três partes:
O símbolo antes dos métodos e atributos representa a visibilidade deles. Os símbolos e suas visibilidades são:
+ Público
# Protegido
~ Pacote
- Privado
A seta vazada (existem 3 delas no exemplo) indicam relações de generalização e é usado para indicar se uma classe herda de outra.
Para esta aula iremos usar somente essa pequena parte do padrão de Diagrama de Classes, caso você queira saber mais pode ver este simples tutorial.
In [123]:
ValueError.__name__
Out[123]:
E herdam de Exception
:
In [125]:
issubclass(ValueError, Exception)
Out[125]:
Podemos criar nossas próprias exceções herdando de Exception
:
In [126]:
class MeuErro(Exception):
pass
In [128]:
raise MeuErro('deu erro')
Geralmente não precisamos sobreescreve métodos da superclasse Exception
, pois muitas vezes criamos exceções para deixar o programa mais claro. Caso você queira criar um erro com novas funcionalidades consulte a documentação de Exception e a parte tutorial do python que fala sobre exceções.
In [4]:
a, b = 10, 0
a / b
In [5]:
try:
a / b
except ZeroDivisionError:
print('Olha a divisão por zero aí mano!')
Também é possível multiplicar múltiplas exceções a serem tratadas:
In [7]:
b = "0"
try:
a / b
except (ZeroDivisionError, TypeError):
print('Erro: tem que ver isso daí')
Porém lidar com exceções dessa maneira não é recomendado, pois isso pode omitir o nome da exceção e mascarar erros reais! Um jeito melhor de trabalhar com exceções é tratar cada uma de uma maneira:
In [13]:
b = "0"
try:
a / b
except ZeroDivisionError:
print('Não é possível dividir por 0')
except TypeError:
print('Algum tipo está errado')
In [12]:
b = 0
try:
a / b
except ZeroDivisionError:
print('Não é possível dividir por 0')
except TypeError:
print('Algum tipo está errado')
É possível especificar uma variável para ser atribuída a uma exceção levantada e melhorar o tratamento do erro:
In [27]:
a = {}
try:
print(a['chave'])
except KeyError as exc:
print(type(exc)) # imprime o tipo da exceção
print(exc.args) # mostra a tupla de argumentos que a exceção recebeu
print(exc) # imprime diretamente os argumento
Para finalizar segue a lista de exceções padrão. A explicação de cada exceção está na documentação:
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning
O Python oferece suporte a herança múltipla. A forma de usar herança múltipla é parecida com a herança simples, porém a subclasse herda de mais de uma classe, por exemplo:
class Subclasse(Base1, Base2, Base3):
...
...
Algumas linguagens famosas não implementam herança múltipla como Java, C# e Ruby (elas possuem outras técnicas para prover essa funcionalidade), pois quando usada de forma incorreta pode resultar em sistemas ambíguos e difíceis de entender.
Um problema muito comum de herança múltipla é o Problema do losango, em que aparece ambiguidade quando uma classe A implementa um método que é sobrescrito nas subclasses B e C e quando D, que não sobrescreveu esse método, invoca esse método qual deve ser chamado: o de C ou de D?
Vamos fazer um exemplo que implementa o problema do losango e mostaremos como o Python o resolve:
In [3]:
class A:
def ping(self):
print('ping', self)
class B(A):
def pong(self):
print('pong', self)
class C(A):
def pong(self):
print('PONG', self)
class D(B, C):
def ping(self):
super().ping()
print('post-ping:', self)
def pingpong(self):
self.ping()
super().ping()
self.pong()
C.pong(self)
In [5]:
d = D()
d.pong()
Como vemos, o método chamado pela instância da classe D foi B.pong()
, p|odemos chamar explicitamente C.pong()
passando a instância como argumento explícito:
In [6]:
C.pong(d)
Essa maneira de chamar métodos só é utilizando para resolver ambiguidade causada pela herança múltipla. O jeito recomendado de delegar chamadas de métodos a superclasses é usar a função embutida super()
, pois é mais seguro e resistente a mudanças futuras, principalmente quando chamamos métodos em um framework ou em qualquer hierarquia de classes sobre a qual você não tenha controle.
Vamos ver como a função D.ping()
se comporta:
In [34]:
d.ping()
d.ping()
chamou a função A.ping()
(que imprimiu ping
) e imprimiu post-ping
Agora vamos analisar as chamadas do método pingpong
:
In [35]:
d.pingpong()
Primeiro foi chamado D.ping()
que chama A.ping()
e imprime post-ping
Depois foi chamado super().ping()
que chama somente A.ping()
Foi chamado super().pong()
que chamou B.pong()
E, por fim, C.pong(self)
foi chamado diretamente, passando a instância de D
.
Agora que entendemos como esse exemplo do problema do diagrama é rodado, podemos falar sobre como o Python resolve essa ambiguidade: percorrendo o grafo de herança de uma forma específica: o MRO - Method Resolution Order ou Ordem de Resolução dos Métodos.
O Python procura o método na própria classe, depois em cada superclasse (sem repetição, caso exista haja sobreposição na hierarquia) da esquerda para a direita, depois procura nas superclasses da superclasses da esquerda para direita até chegar a object
(herdado por todas as classes).
Todos os objetos das classes possuem um atributo chamado __mro__
que apresenta uma tupla de referências às superclasses na ordem MRO, da classe atual até a classe object
. Vamos ver qual é o MRO da classe D
:
In [18]:
D.__mro__
Out[18]:
Aqui comprovamos o que foi dito anteriormente: primeiro um atributo é buscado na seguinte ordem: D, B, C, A e object.
Vamos inspecionar o atributo __mro__
de outras classes:
In [36]:
bool.__mro__
Out[36]:
In [40]:
from numbers import Integral
Integral.__mro__
Out[40]:
In [41]:
from io import StringIO
StringIO.__mro__
Out[41]:
Como dito anteriormente a herança múltipla pode tornar o código de um software muito complicado e frágil. A seguir consta algumas dicas para deixar grafos de classes mais simples:
Faça a distinção entre herança de interface (para criar um subtipo, implicando em uma relação "é um") e herança de implementação (para evitar duplicação de código por meio de reutilização)
Deixe as interfaces explícitas com ABCs
Use mixins e explicite as mixins pelo nome
Uma ABC pode ser um mixin, mas não o contrário
Não herde de mais de uma classe concreta
Ofereça classes agregadas aos usuários
"Prefira composição de objetos à herança de classe" (GoF, Padrões de Projeto)
"Mixin é uma classe que provê funcionalidade para ser herdada, mas não instanciada sozinha. [...] pode ser usada para melhorar funcionalidades e comportamentos de classes" (Greenfield e Greenfield, Two Scoops of Django 1.8)
Regra de uso de mixins:
O django é um framework web Python muito popular. Ele facilita a criação de views oferecendo views genéricas baseadas em classe - as Class Based Views (CBV - que implementam tarefas comuns na criação de sistema webs como listar objetos, exibir páginas estáticas e criar views de redirecionamento.
As Class Based Views foram criadas usando herança múltipla e seguem as dicas citadas acima. Abaixo segue o Diagrama de Classes para podermos entender como a herança múltipla pode ser usada de maneira positiva na prática:
In [4]:
from django.views.generic import TemplateView
TemplateView.__mro__
Out[4]:
Agora verificaremos o RedirectView.__mro__
:
In [5]:
from django.views.generic import RedirectView
RedirectView.__mro__
Out[5]:
In [6]:
from django.views.generic import DetailView
DetailView.__mro__
Out[6]:
In [7]:
from django.views.generic import ListView
ListView.__mro__
Out[7]:
In [8]:
from django.views.generic import DeleteView
DeleteView.__mro__
Out[8]:
Fim da aula 02